package com.inspinia.message.rpc.service;

import cn.hutool.core.collection.CollectionUtil;
import com.google.common.collect.Lists;
import com.inspinia.base.cache.RedisUtil;
import com.inspinia.base.common.model.UserCacheKeys;
import com.inspinia.base.dao.BaseDao;
import com.inspinia.base.heartbeat.util.BrowserHeartbeatObserver;
import com.inspinia.base.heartbeat.util.BrowserHeartbeatUtil;
import com.inspinia.base.service.BaseServiceImpl;
import com.inspinia.base.util.JsonUtils;
import com.inspinia.base.util.StringUtils;
import com.inspinia.message.common.model.*;
import com.inspinia.message.common.util.AppMessagePush;
import com.inspinia.message.common.util.MessagePushUtil;
import com.inspinia.message.dao.NoticeDao;
import com.inspinia.message.rpc.api.NoticeAddresseeService;
import com.inspinia.message.rpc.api.NoticePushService;
import com.inspinia.message.rpc.api.NoticeService;
import com.inspinia.upms.common.model.user.UpmsUser;
import com.inspinia.upms.rpc.api.UpmsDeptService;
import com.inspinia.upms.rpc.api.UpmsRoleService;
import com.inspinia.upms.rpc.api.UpmsUserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import redis.clients.jedis.Jedis;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 通知的服务层实现类
 * Created by heshun on 2018-1-8.
 */
@Service
public class NoticeServiceImpl extends BaseServiceImpl<Notice> implements NoticeService {
    /**
     * 过期的缓存的key
     */
    public static final String CACHE_KEY_EX_NOTICE = "notice";
    @Resource
    private NoticeDao noticeDao;
    @Resource
    private NoticeAddresseeService noticeAddresseeService;
    @Resource
    private UpmsDeptService departmentService;
    @Resource
    private UpmsRoleService roleService;
    @Resource
    private UpmsUserService userService;
    @Resource
    private NoticePushService noticePushService;
    @Resource
    private KeyExpiredListener keyExpiredListener;

    @Override
    protected BaseDao<Notice> getDao() {
        return noticeDao;
    }

    @Override
    public int insert(Notice bean) {
        int i = super.insert(bean);
        saveExpiredKey(bean.getId(), bean.getValidityPeriodStart(), false);
        return i;
    }

    /**
     * 保存一个会过期的key，如果key过期，系统就会收到redis发送的通知。在init()中已经监听了redis的key过期事件。
     *
     * @param id
     * @param validityPeriodStart
     */
    private void saveExpiredKey(Long id, Date validityPeriodStart, boolean forUpdate) {
        String key = genCacheKey(id);
        if (validityPeriodStart != null) {
            RedisUtil.set(key, String.valueOf(id), calcExpiredTime(validityPeriodStart));
        } else if (forUpdate) {
            //修改的时候需要删除key，避免触发key过期事件
            RedisUtil.delete(key);
        }
    }

    /**
     * 计算过期时间
     *
     * @param validityPeriodStart
     * @return
     */
    private int calcExpiredTime(Date validityPeriodStart) {
        Long second = (validityPeriodStart.getTime() - System.currentTimeMillis()) / 1000;
        return second.intValue();
    }

    private String genCacheKey(Long id) {
        return CACHE_KEY_EX_NOTICE + ":" + id;
    }


    @Override
    public int update(Notice bean) {
        int update = super.update(bean);
        saveExpiredKey(bean.getId(), bean.getValidityPeriodStart(), true);
        return update;
    }

    @PostConstruct
    public void init() {
        new Thread(() -> {
            Jedis jedis = RedisUtil.getPool().getResource();
            jedis.psubscribe(keyExpiredListener, "__keyevent@*:expired");
        }).start();

        //监听用户上线事件，如果用户上线，需要给用户推送未过期的消息
        BrowserHeartbeatUtil.addHeartbeatObserver(new BrowserHeartbeatObserver() {

            @Override
            public void onLostHeartbeat(Long userId) {

            }

            @Override
            public void onGetFirstHeartbeat(Long userId) {
                rePush2User(userId);
            }
        });

    }

    /**
     * 将用户未读的消息重新推送给用户
     *
     * @param userId
     */
    private void rePush2User(Long userId) {
        List<Notice> notices = findTopNUnread(userId, 10);
        logger.info("push notice to user(" + userId + "),notices size is " + notices.size());

        if (CollectionUtil.isNotEmpty(notices)) {

            int reduced = 0;
            for (int i = 0; i < notices.size(); i++) {
                Notice notice = notices.get(i);
                if (notice.isExpired()) {

                    //没有必要让用户看到一条已过期的消息，因此在这里就将该推送记录删掉。
                    noticePushService.delete(notice.getNoticePushId());
                    i--;
                    reduced++;
                } else if (!notice.pushable()) {
                    i--;
                }
            }

            if (CollectionUtil.isNotEmpty(notices)) {
                pushNoticeCount(userId, reduced, notices.size());
                MessagePushUtil.pushJsonMessage(userId, notices);
            }
        }
    }

    private void pushNoticeCount(Long userId, int reduced, int pushed) {
        Notice notice = new Notice();
        notice.setLinkType(NoticeLinkType.noticeCount);
        notice.setAddresseeType(NoticeAddresseeType.personal);
        NoticeCountLinkData linkData = new NoticeCountLinkData(countUnread(userId) - (reduced + pushed));
        //总消息数 = 未读数量 - （删掉的数量 + 本次推送的数量）
        notice.setLinkData(JsonUtils.toJson(linkData));
        MessagePushUtil.pushJsonMessage(userId, notice);
    }

    /**
     * 找出最近n条未读的通知
     *
     * @param userId
     * @param topN
     * @return
     */
    private List<Notice> findTopNUnread(Long userId, int topN) {
        List<NoticePush> notices = noticePushService.findTopNUnreadNoticeId(userId, topN);
        List<Notice> newList = notices.stream().map(np -> {
            Notice notice = get(np.getNoticeId());

            //理论上不应该出现null，但是正式环境出现了，这里做一个简单的数据适配
            if (notice == null) {
                return null;
            }
            notice.setAddresseeId(np.getAddresseeId());
            notice.setNoticePushId(np.getId());
            return notice;
        }).collect(Collectors.toList());

        //移除null值
        newList.removeAll(Collections.singleton(null));
        return newList;
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void push(Long id, NoticeAddresseeType addresseeType, Collection<Long> addresseeIds) {

        List<Long> noticeAddIds = noticeAddresseeService.batchInsert(id, addresseeType, addresseeIds);
        if (CollectionUtil.isNotEmpty(noticeAddIds)) {
            Notice notice = get(id);
            if (notice.pushable()) {
                pushAndSaveLog(id, findUserIds(addresseeType, addresseeIds));
                noticeAddresseeService.setPushed(noticeAddIds);
            }
        }

    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void push(Notice bean, NoticeAddresseeType addresseeType, Collection<Long> addresseeIds) {
        insert(bean);
        push(bean.getId(), addresseeType, addresseeIds);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void pushAndSaveLog(Long id, Collection<Long> userIds) {
        if (CollectionUtil.isEmpty(userIds)) {
            return;
        }

        //保存推送记录
        List<Long> noticePushIds = noticePushService.batchInsert(id, userIds);

        int i = 0;
        for (Long userId : userIds) {
            Notice notice = get(id);
            notice.setNoticePushId(noticePushIds.get(i));
            notice.setAddresseeId(userId);
            pus2AppOrWeb(userId, notice);
            i++;
        }
    }

    private void pus2AppOrWeb(Long userId, Notice notice) {
        switch (notice.getLinkType().getClientCombination()) {
            case JUST_APP:
                push2App(userId, notice);
                break;
            case JUST_WEB:
                push2Web(userId, notice);
                break;
            default:
                push2App(userId, notice);
                push2Web(userId, notice);
                break;
        }

    }

    private void push2Web(Long userId, Notice notice) {
        MessagePushUtil.pushJsonMessage(userId, notice);
    }

    private void push2App(Long userId, Notice notice) {
        try {
            UpmsUser user = userService.get(userId);
            String usernameSessionKey = UserCacheKeys.getUsernameSessionKey(user.getUserName() + "_app");
            String key = RedisUtil.get(usernameSessionKey);
            if (StringUtils.isNotBlank(key)) {
               AppMessagePush.pushMessageToApp(userId, notice);
            }
        } catch (Exception e) {
            logger.info(userId + "的消息推送失败：消息类型为" + notice.getLinkType().getCode());
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void push(Long id) {

        //查找没有推送过的接收人及id
        List<NoticeAddressee> addressees = noticeAddresseeService.findUnPushed(id);
        if (CollectionUtil.isEmpty(addressees)) {
            return;
        }
        Set<Long> userIds = filterRepeatAddressId(addressees);
        if (CollectionUtil.isNotEmpty(userIds)) {
            //推送至客户端
            pushAndSaveLog(id, userIds);

            //设置为已推送
            List<Long> addIds = new ArrayList<>(addressees.size());
            addressees.forEach(a -> addIds.add(a.getId()));
            noticeAddresseeService.setPushed(addIds);
        }
    }

    @Override
    public void push(Collection<Long> addresseeIds, NoticeLinkType linkType, Object linkData, String content) {
        if (CollectionUtil.isNotEmpty(addresseeIds)) {
            Notice notice = genNotice(linkType, linkData, content);
            push(notice, notice.getAddresseeType(), addresseeIds);
        }
    }

    @Override
    public void push(Long addressId, NoticeLinkType linkType, Object linkData, String content) {
        if (addressId != null) {
            push(Lists.newArrayList(addressId), linkType, linkData, content);
        }
    }

    @Override
    public int countUnread(Long userId) {
        return noticePushService.countUnread(userId);
    }

    @Override
    public void prompt(Collection<Long> addresseeIds, NoticeLinkType linkType, Object linkData, String content) {
        Notice notice = new Notice(linkType.getName(), NoticeType.sys, content, linkType, JsonUtils.toJson(linkData), NoticeAddresseeType.personal);
        MessagePushUtil.pushJsonMessage(addresseeIds, notice);
    }


    /**
     * 过滤重复的收件人id
     *
     * @param addressees
     * @return
     */
    private Set<Long> filterRepeatAddressId(Collection<NoticeAddressee> addressees) {
        Map<NoticeAddresseeType, Set<Long>> typeAddresseeIdMap = new HashMap<>();
        for (NoticeAddressee addressee : addressees) {
            Set<Long> longs = typeAddresseeIdMap.get(addressee.getAddresseeType());
            if (longs == null) {
                longs = new HashSet<>();
                typeAddresseeIdMap.put(addressee.getAddresseeType(), longs);
            }
            longs.add(addressee.getAddresseeId());
        }
        Set<Long> userIds = new HashSet<>();
        typeAddresseeIdMap.forEach((k, v) -> userIds.addAll(findUserIds(k, v)));
        return userIds;
    }

    private Set<Long> findUserIds(NoticeAddresseeType addresseeType, Collection<Long> addresseeIds) {
        Set<Long> userIds;
        switch (addresseeType) {
            case all:
                userIds = new HashSet(userService.findAllUserIds());
                break;
            case org:
                List<Long> deptIdsByOrgIds = departmentService.findDeptIdsByOrgIds(addresseeIds);
                userIds = new HashSet<>(userService.findUserIdsByDeptIds(deptIdsByOrgIds));
                break;
            case role:
                userIds = new HashSet<>();
                for (Long addresseeId : addresseeIds) {
                    userIds.addAll(roleService.findUserIdsByRoleId(addresseeId));
                }
                break;
            case dept:
                userIds = new HashSet<>(userService.findUserIdsByDeptIds(addresseeIds));
                break;
            default:
                userIds = new HashSet<>(addresseeIds);
                break;
        }
        return userIds;
    }

    private Notice genNotice(NoticeLinkType linkType, Object linkData, String content) {
        Notice notice;
        if (linkData == null) {
            linkData = "";
        }
        switch (linkType) {
            default:
                notice = new Notice(linkType.getName(), NoticeType.normal, content, linkType, JsonUtils.toJson(linkData), NoticeAddresseeType.personal);
                break;
        }
        return notice;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteDayBefore(int day) {
        noticeAddresseeService.deleteDayBefore(day);
        noticePushService.deleteDayBefore(day);
        noticeDao.deleteDayBefore(day);
    }
}
